﻿package de.gameduell.framework.threads {
	import de.gameduell.framework.modules.ModuleAdapter;
	import de.gameduell.framework.modules.ModuleHeartbeat;
	import de.gameduell.framework.util.HashMap;

	import flash.events.Event;

	/**
	 * this manager receives StartThreadRequest's from other modules, let the callback executing as long it is blocking, 
	 * finally inform all other modules about finished.
	 * 
	 * documentation:
	 * http://de.wikipedia.org/wiki/Mutex
	 * http://de.wikipedia.org/wiki/Thread_%28Informatik%29#Threaddarstellung_in_UML
	 * http://de.wikipedia.org/wiki/Mutex#Aktives_und_passives_Warten
	 * 
	 * @author Ferry Trunschke
	 */
	public class ThreadPoolManager extends ModuleAdapter {
		
		// main GD action queue
		public static const MAIN_QUEUE:String = "mainQueue";

		// popup queue
		public static const POPUP_QUEUE:String = "popupQueue";

		// chat message queue
		public static const CHAT_QUEUE:String = "chatQueue";

		// internal queue, reserved for all blocking requests
		public static const INTERNAL_ALL_BLOCKING_QUEUE:String = "internalQueue";
		
		// key=queueId value=[IBlockingThread, ...]
		public var queueMap:HashMap = new HashMap();
		
		// key=queueId value=[IBlockingThread, ...]
		public var joinMap:HashMap = new HashMap();
		
		// key=queueId value=IBlockingThread
		public var activeCallbackMap:HashMap = new HashMap();
		
		// refresh rate / heartbeat 
		private var refreshFps:int = 10;
		
		// update non-blocking threads as long they are active
		private var nonBlockingThreadQueue:Array = [];
		
		
		
		public function ThreadPoolManager()  {
		}
		
		/**
		 * store context references for internal use and child creation.
		 **/		
		public override function init(contextMap:HashMap):void {
			super.init(contextMap);
			
			// init refreshRate from config
			if (contextMap.containsKey("ThreadPoolManager.refreshFps")) {
				refreshFps = contextMap.getValue("ThreadPoolManager.refreshFps") as int;
			}
			
			// start heartbeat for callback manager (activate timerUpdate)
			var heartbeat:ModuleHeartbeat = new ModuleHeartbeat(this);
			heartbeat.startTimer(refreshFps);
		}
		
		/**
		 * an event occurs
		 **/
		public override function handleEvent(command:Event = null):void {
			
			// start blocking callback
			if (command is StartThreadRequest) {
				var request:StartThreadRequest = command as StartThreadRequest;
				
				// add to local queue (to generate stop event when finished)
				if (request.thread.isBlockingThread) {
					
					// join currently blocking thread (only when queue has a blocking thread, otherwise just start a new thread) ?
					if (request.joinBlockingThread && getQueue(request.queueId).length > 0) {
						
						// add to joining queue
						getJoiningThreads(request.queueId).push(request.thread);
						
						// start instant
						request.thread.onStartExecution();
						
					}
					// add new blocking thread
					else {
						
						// add on tail
						getQueue(request.queueId).push(request.thread);
						
						// start directly when queue is empty,
						// - avoid queue between following poll actions
						if (getQueue(request.queueId).length == 1) {
							
							// start instant
							request.thread.onStartExecution();
							
							// start with new callback
							activeCallbackMap.put(request.queueId, request.thread);						
						}
					}
					
				}
				// execute non-blocking
				else {
					
					// schedule for execution
					nonBlockingThreadQueue.push(request.thread);
					
					// start directly
					request.thread.onStartExecution();
				}
			}
			
		}		

		/**
		 * timer update
		 **/
		public override function timerUpdate(subFrames:int, dt:Number):void {
			
			// check all queues parallel
			for each(var queueId:String in queueMap.keys) {
				var queue:Array = getQueue(queueId);
				var joiningThreads:Array = getJoiningThreads(queueId);
				
				// update joins threads and check if all active
				var joiningThreadsActive:Boolean = false;
				for each(var joiningThread:IThread in joiningThreads) {
					if (joiningThread.run()) {
						joiningThreadsActive = true;	
					}
				}
				
				// has elements ?
				if (queue.length > 0) {
					
					// take head of this queue
					var callback:IThread = queue[0] as IThread;
					
					// last active callback
					var lastActiveCallback:IThread = activeCallbackMap.getValue(queueId) as IThread;
					
					// start new active callback?
					if (lastActiveCallback == null || lastActiveCallback != callback) {
						
						// callback on start
						callback.onStartExecution();
						
						// start with new callback
						activeCallbackMap.put(queueId, callback);
					}
					
					
					// blocking finished?
					if (!callback.run() && !joiningThreadsActive) {
						
						// callback on finish
						callback.onFinishExecution();
						
						// drop from queue
						queue.shift();
						
						// inform all other modules
						broadcastEvent(new FinishThreadResponse(queueId, callback));
						
					}
				}
			}
			
			// update non-blocking async threads, collect dead threads for droppping
			var finishedNonBlockingThreads:Array = [];
			for each(var thread:IThread in nonBlockingThreadQueue) {
				// update, check if alive
				if (!thread.run()) {
					finishedNonBlockingThreads.push(thread);
				}
			}
			
			// drop dead threads
			for each(var dropThread:IThread in finishedNonBlockingThreads) {
				var dropIndex:int = nonBlockingThreadQueue.indexOf(dropThread);
				if (dropIndex != -1) {
					nonBlockingThreadQueue.splice(dropIndex, 1);
				}
				
				// inform all other modules (annonymous queue)
				broadcastEvent(new FinishThreadResponse(null, dropThread));
			}
			
		}	
		
		protected function getQueue(queueId:String):Array {
			if (!queueMap.containsKey(queueId)) {
				queueMap.put(queueId, []);
			}
			
			return queueMap.getValue(queueId) as Array;
		}
		
		protected function getJoiningThreads(queueId:String):Array {
			if (!joinMap.containsKey(queueId)) {
				joinMap.put(queueId, []);
			}
			
			return joinMap.getValue(queueId) as Array;
		}
		
		// xml formated, \n line breaks to show in debug monitor
		public function getPoolDump():String {
			
			var result:String = "";
			result += "<pool>\n";
			
			// dump non blocking queue
			result += "\t<nonBlocking>\n";
			for each(var nonBlockingThread:IThread in nonBlockingThreadQueue) {
				result += "\t\t<" + nonBlockingThread.description + "/>\n"; 
			}
			result += "\t</nonBlocking>\n";

			// dump blocking queues
			for each(var queueId:String in queueMap.keys) {
				var queue:Array = getQueue(queueId);
				
				result += "\t<" + queueId + ">\n";
				for each(var blockingThread:IThread in queue) {
					result += "\t\t<" + blockingThread.description + "/>\n"; // " class="+ getQualifiedClassName(blockingThread) 
				}
				result += "\t</"+queueId+">\n";
			}
			
			result += "</pool>";
			
			return result;
		}
		
	}
}